﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels;
using CommonTypes;
using System.Collections;
using System.Threading;
using System.Runtime.Serialization.Formatters;
using System.Net.Sockets;
using System.Timers;

namespace ServerApp
{
    class ServerApp
    {
        public static System.Timers.Timer aTimer = new System.Timers.Timer(100);
        public static IServer myRef;
        public static IServer serverRef;
        static void Main(string[] args)
        {
            String ipPort = args[1];
            int port = Int32.Parse(ipPort.Split(new Char[] { ':' })[1]);
            BinaryServerFormatterSinkProvider clientChannelProvider = new BinaryServerFormatterSinkProvider();
            clientChannelProvider.TypeFilterLevel = TypeFilterLevel.Full;
            IDictionary clientChannelProps = new Hashtable();
            clientChannelProps["name"] = "ClientChannel";

            TcpChannel clientChannel = new TcpChannel(clientChannelProps, null, clientChannelProvider);
            ChannelServices.RegisterChannel(clientChannel, true);
            //Verifica se existe alguma replica no porto em que a replica vai ser lançada
            //se existe lança uma notificação para que o porto seja libertado
            try
            {
                serverRef = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + port + "/Server");
                serverRef.notifyWakeUp();
            }
            catch (SocketException e)
            {
                Console.WriteLine(e.Message);
            }

            BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
            IDictionary props = new Hashtable();
            provider.TypeFilterLevel = TypeFilterLevel.Full;
            props["port"] = port;
            props["name"] = "MyPort";
            props["timeout"] = 1;
            TcpChannel channel = new TcpChannel(props, null, provider);
            ChannelServices.RegisterChannel(channel, true);

            //Registar o objecto
            Server myRef = new Server();
            ObjRef ref1 = RemotingServices.Marshal(myRef, "Server");

            //RemotingConfiguration.RegisterWellKnownServiceType(typeof(Server), "Server", WellKnownObjectMode.Singleton);
            //RemotingConfiguration.RegisterWellKnownClientType(typeof(Server),"tcp://localhost:"+port+"/Server");


            int[] ports = new int[] { port - 1, port + 1 };
            int i = 1;
            IServer replicaRef;
            //Verificar se existe alguma replica activa
            foreach (int replicaPort in ports)
            {
                try
                {
                    //Se estiver activa uma replica no port-1 a replica em criação será bacukp da primeira
                    replicaRef = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + replicaPort + "/Server");
                    if (i == 1)
                    {
                        //myRef = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + port + "/Server");
                        replicaRef.setBackup(port);
                        replicaRef.sendUsersList(port);
                        replicaRef.sendTicketCounter(port);
                        myRef.setPrimary(replicaPort);
                    }
                    //Se estiver activa uma replica no port+1 a replica em criação será primary da primeira
                    if (i == 2)
                    {
                        replicaRef.setPrimary(port);
                        replicaRef.sendUsersList(port);
                        replicaRef.sendTicketCounter(port);
                        //myRef = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + port + "/Server");
                        myRef.setBackup(replicaPort);
                    }
                }
                catch (SocketException e)
                {
                    Console.WriteLine(e.Message);
                }
                i++;
            }
            Console.WriteLine("Servidor {0} iniciado com sucesso!", args[0]);
            Console.ReadLine();

        }

        public class Server : MarshalByRefObject, IServer
        {

            //Properties ticket counter
            private Ticket ticketCounter = new Ticket(0);
            private Dictionary<String, ClientInfo> clients = new Dictionary<string, ClientInfo>();
            private IServer myBackupServer = null;
            private ServerInfo myPrimaryServer = null;
            //Timer para temporizar a comunicação entre as replicas(update msgs)
            private System.Timers.Timer aTimer = new System.Timers.Timer(100);
            private TcpChannel primaryChannel;


            //connecta um cliente
            public String connect(String username, String ipPort)
            {

                IClient clientRef = (IClient)Activator.GetObject(typeof(IClient), "tcp://" + ipPort + "/" + username);

                try
                {
                    clients.Add(username, new ClientInfo(clientRef, username, ipPort, null));
                }
                catch (ArgumentException)
                {
                    clients[username].ClientRef = clientRef;
                    clients[username].IpPort = ipPort;
                    clients[username].IsOnline = true;
                }

                Console.WriteLine("O cliente " + username + " registou-se com sucesso e está disponível no IP: " + ipPort);
                if (myBackupServer != null)
                {
                    try
                    {
                        //notificar a acção ao backup
                        myBackupServer.connect(username, ipPort);
                    }
                    catch (SocketException e)
                    {
                        myBackupServer = null;
                    }

                }
                return "[Conectado com sucesso!]\r\n";
            }
            //desconnecta um cliente
            public String disconnect(String username)
            {
                Console.WriteLine("Ola");
                clients[username].IsOnline = false;
                Console.WriteLine("O cliente " + username + " disconectou-se!!");

                try
                {
                    //notificar a acção ao backup
                    if (myBackupServer != null)
                        myBackupServer.disconnect(username);

                }
                catch (SocketException e)
                {
                    myBackupServer = null;
                }
                return "[Desconectado com sucesso!]";
            }
            //fornece o endereço de um cliente
            public String getClientRef(String username)
            {
                ClientInfo clientInfo = clients[username];
                if (clientInfo.IsOnline)
                    return clients[username].IpPort;
                else
                    return null;
            }
            //fornece um ticket
            public int grantTicket()
            {
                lock (ticketCounter)
                {
                    ticketCounter.incCount();
                    if (myBackupServer != null)
                    {
                        try
                        {
                            //notificar a acção ao backup
                            myBackupServer.updateTicketCounter(ticketCounter.Counter);
                        }
                        catch (SocketException e)
                        {
                            myBackupServer = null;
                        }
                    }
                    Console.WriteLine("Ticket: " + ticketCounter.Counter);
                    return ticketCounter.Counter;
                }
            }
            //permite enviar o ticketcounter a uma replica no porto port
            public void sendTicketCounter(int port)
            {
                lock (ticketCounter)
                {
                    IServer server = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + port + "/Server");
                    try
                    {
                        server.updateTicketCounter(ticketCounter.Counter);
                    }
                    catch (SocketException e)
                    {
                        Console.WriteLine(e.Message);
                    }
                }

            }
            //metodo que permite a uma replica receber o estado do ticketcounter (quando se inicia)
            public void updateTicketCounter(int counter)
            {
                lock (ticketCounter)
                {
                    ticketCounter.Counter = counter;
                    Console.WriteLine("Update counter: " + ticketCounter.Counter);
                }
            }
            //permite enviar a lista users connectados a uma replica no porto port
            public void sendUsersList(int port)
            {
                IServer server = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + port + "/Server");
                List<ClientInfo> usersOnlineList = new List<ClientInfo>();

                foreach (ClientInfo user in clients.Values)
                {
                    if (user.IsOnline)
                    {
                        usersOnlineList.Add(user);
                    }
                }

                try
                {
                    server.updateUsersList(usersOnlineList);
                }
                catch (SocketException e)
                {
                }
            }
            //metodo que permite a uma replica receber o users que estão online (quando se inicia)
            public void updateUsersList(List<ClientInfo> clients)
            {
                foreach (ClientInfo user in clients)
                    connect(user.Username, user.IpPort);
            }
            //serve para verificar se um servidor está "vivo"
            public String echo()
            {
                Console.WriteLine("Server I'm alive");
                return "I'm alive";
            }
            //define qual o replica backup para uma determinada replica
            public void setBackup(int backupPort)
            {
                myBackupServer = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + backupPort + "/Server");
                Console.WriteLine("MyBackup is @" + backupPort);
            }
            //define qual o replica primaria para uma determinada replica
            public void setPrimary(int primaryPort)
            {
                IServer myPrimaryServerRef = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + primaryPort + "/Server");
                myPrimaryServer = new ServerInfo(null, myPrimaryServerRef, "" + primaryPort, null);
                lock (aTimer)
                {
                    aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
                    aTimer.Enabled = true;
                }
                Console.WriteLine("MyPrimary is @" + primaryPort);
            }
            //notifica que uma replica "acordou"
            public void notifyWakeUp()
            {
                lock (primaryChannel)
                {
                    primaryChannel.StopListening(new Object());
                    ChannelServices.UnregisterChannel(primaryChannel);
                }
            }
            //metodo que permite que o objecto remoto permaneça "vivo infinitamente" 
            public override object InitializeLifetimeService()
            {
                return null;
            }
            //metodo invocado a cada "tick" do relogio
            private void OnTimedEvent(object source, ElapsedEventArgs e)
            {
                try
                {
                    //verifica se a respectiva replica primária está "viva"
                    Console.WriteLine(myPrimaryServer.Server.echo());
                }
                catch (SocketException exception)
                {
                    
                    lock (aTimer)
                    {
                        aTimer.Enabled = false;
                    }
                    IServer server = (IServer)Activator.GetObject(typeof(IServer), "tcp://localhost:" + (Int32.Parse(myPrimaryServer.IpPort)-1) + "/Server");
                    //Caso a respectiva replica primaria nao esteja "viva"
                    //tenta falar com a replica primaria da sua replica primaria
                    try
                    {
                        server.echo();
                        server.setBackup(Int32.Parse(myPrimaryServer.IpPort) + 1);
                        myPrimaryServer.Server = server;
                        myPrimaryServer.Server.sendUsersList(Int32.Parse(myPrimaryServer.IpPort) + 1);
                        myPrimaryServer.Server.sendTicketCounter(Int32.Parse(myPrimaryServer.IpPort) + 1);
                        lock (aTimer)
                        {
                            aTimer.Enabled = true;
                        }
                    }
                    catch (SocketException excptn) {
                        //caso a replica primaria da minha replica primaria não estiver activa
                        //passo a escutar no porto da minha replica primaria
                        BinaryServerFormatterSinkProvider primaryChannelProvider = new BinaryServerFormatterSinkProvider();
                        primaryChannelProvider.TypeFilterLevel = TypeFilterLevel.Full;
                        IDictionary primaryChannelProps = new Hashtable();
                        primaryChannelProps["name"] = "PrimaryChannel";
                        primaryChannelProps["port"] = Int32.Parse(myPrimaryServer.IpPort);
                        primaryChannel = new TcpChannel(primaryChannelProps, null, primaryChannelProvider);
                        ChannelServices.RegisterChannel(primaryChannel, true);
                        myPrimaryServer = null;
                        Console.WriteLine(exception.Message);
                    }
                }
            }
            //Classe que representa o "Ticket counter"
            public class Ticket {
                private int ticketCounter;

                public Ticket(int counter) {
                    ticketCounter = counter;
                }

                public int Counter {
                    get { return ticketCounter; }
                    set { ticketCounter = value; }
                }

                public int incCount() {
                    ticketCounter++;
                    return ticketCounter;
                }
            }
        }
    }
}